/*
 * Copyright (C) 2006 Johan Maasing johan at zoom.nu Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
 * or agreed to in writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package nu.zoom.catonine.swing.tail;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Desktop;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.swing.Action;
import javax.swing.Box;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JSeparator;
import javax.swing.JTextPane;
import javax.swing.JToolBar;
import javax.swing.JViewport;
import javax.swing.KeyStroke;
import javax.swing.SwingConstants;
import javax.swing.text.DefaultEditorKit;
import javax.swing.text.EditorKit;

import nu.zoom.catonine.desktop.DelayedCursorSwitcher;
import nu.zoom.catonine.prefs.Preferences;
import nu.zoom.catonine.stylerule.StyleRule;
import nu.zoom.catonine.stylerule.StyleRuleListener;
import nu.zoom.catonine.stylerule.StyleRules;
import nu.zoom.catonine.stylerule.StyleRulesListModel;
import nu.zoom.catonine.stylerule.StyleRulesListener;
import nu.zoom.catonine.stylerule.StyleRulesManager;
import nu.zoom.catonine.swing.tail.gutter.GutterDocument;
import nu.zoom.catonine.swing.tail.gutter.GutterView;
import nu.zoom.catonine.tail.FullFileTailer;
import nu.zoom.catonine.tail.TailerListener;
import nu.zoom.gal.error.ErrorReporter;
import nu.zoom.gal.progress.Progress;
import nu.zoom.swing.desktop.common.BackendException;
import nu.zoom.swing.desktop.preferences.InvalidDataTypeException;
import nu.zoom.ui.Resources;
import nu.zoom.ui.Resources.ResourceNotFoundException;

/**
 * The main panel of the tailing component.
 * 
 * @author "Johan Maasing" &lt;johan@zoom.nu&gt;
 */
public class TailPane extends JPanel implements StyleRulesListener,
		StyleRuleListener, TailerListener {

	private static final long serialVersionUID = 1L;
	private StyleRuleModel listModel;
	private StyleRulesManager manager = null;
	private final BorderLayout mainLayout = new BorderLayout();
	// private final JScrollPane contentScrollPane;
	private final GutterDocument document;
	private final JTextPane textPane;
	private final TailControlAction tailControlAction;
	private final JComboBox configurationList;
	private StyleRules selectedConfiguration = null;
	private final FullFileTailer tailer;
	private final JLabel tailerMessageLabel;
	private final Icon tailerBusyIcon;
	private final Icon tailerWaitingIcon;
	private final Icon tailerStoppedIcon;
	private final Image foldedImage;
	private final Image unfoldedImage;
	private final int foldImageWidth;
	private final Preferences preferences;
	private final JPopupMenu tailMenu;
	private final DelayedCursorSwitcher cursorSwitcher;
	private final StyleRuleEditCallback styleRuleEditCallback;
	private final AtomicBoolean follow = new AtomicBoolean(true);
	private final AtomicBoolean wrap = new AtomicBoolean(false);
	private final LinesRunner linesRunner;
	private final JPanel noWrapPanel;
	private final JScrollPane textScroller;
	private final AtomicBoolean gutterVisible = new AtomicBoolean(false);
	private GutterView gutterview;
	private CopyToClipboardAction copyToClipboardAction = null;
	private CreateSimpleRuleAction createSimpleRuleAction = null;
	private static final long RESTART_WAIT_TIME = 500;
	private final Timer restartTimer = new Timer(true);
	private final AtomicBoolean restartScheduled = new AtomicBoolean(false);

	/**
	 * Create the tail panel.
	 * 
	 * @param The
	 *            resource bundle to use.
	 * @param additionalToolbarComponents
	 *            Additional components that are to be placed (first) on the
	 *            toolbar.
	 */
	public TailPane(final Resources messages,
			final List<Component> additionalToolbarComponents,
			final Preferences preferences,
			final DelayedCursorSwitcher cursorSwitcher,
			final ErrorReporter errorReporter, final Progress progress,
			final StyleRuleEditCallback styleRuleEditCallback,
			final Font defaultFont) throws ResourceNotFoundException {
		super();
		this.preferences = preferences;
		this.cursorSwitcher = cursorSwitcher;
		this.styleRuleEditCallback = styleRuleEditCallback;

		this.tailerBusyIcon = messages
				.getIcon("nu.zoom.catonine.tail.icon.busy");
		this.tailerWaitingIcon = messages
				.getIcon("nu.zoom.catonine.tail.icon.idle");
		this.tailerStoppedIcon = messages
				.getIcon("nu.zoom.catonine.tail.icon.stopped");
		this.foldedImage = messages.getIcon(
				"nu.zoom.catonine.tail.gutter.folded.img").getImage();
		this.unfoldedImage = messages.getIcon(
				"nu.zoom.catonine.tail.gutter.unfolded.img").getImage();
		this.foldImageWidth = Integer.parseInt(messages
				.getMessage("nu.zoom.catonine.tail.gutter.fold.width"));

		this.document = new GutterDocument(preferences);
		this.document.setDefaultFont(defaultFont);
		this.tailer = new FullFileTailer(preferences);

		setLayout(mainLayout);

		// Build tail -f pane
		this.textPane = new JTextPane(document);
		this.textPane.setEditable(false);
		this.linesRunner = new LinesRunner(progress, messages, textPane,
				document);

		this.noWrapPanel = new JPanel(new BorderLayout());
		this.noWrapPanel.add(textPane);
		setLayout(new BorderLayout());
		this.textScroller = new JScrollPane(noWrapPanel);
		this.textScroller.getViewport().setScrollMode(
				JViewport.BLIT_SCROLL_MODE);
		this.textScroller.getVerticalScrollBar().setUnitIncrement(16);
		this.textScroller.getHorizontalScrollBar().setUnitIncrement(16);
		add(textScroller, BorderLayout.CENTER);
		final TailerStopperListener stopperListener = new TailerStopperListener();
		this.textScroller.getVerticalScrollBar().addMouseListener(
				stopperListener);
		this.textScroller.getVerticalScrollBar().addAdjustmentListener(
				new AdjustmentListener() {

					@Override
					public void adjustmentValueChanged(AdjustmentEvent e) {
						if (e.getValueIsAdjusting()) {
							return;
						}
						if (!isFollow()) {
							int maximum = e.getAdjustable().getMaximum()
									- e.getAdjustable().getVisibleAmount();
							boolean atEnd = (maximum == e.getValue());
							if (atEnd) {
								setFollow(true);
							}
						}
					}
				});
		this.textScroller.getHorizontalScrollBar().addMouseListener(
				stopperListener);
		this.textScroller.addMouseWheelListener(stopperListener);
		this.textPane.addMouseListener(stopperListener);

		tailControlAction = new TailControlAction(this, messages);
		final JButton tailControl = new JButton(tailControlAction);
		tailControl.setVerticalAlignment(SwingConstants.BOTTOM);

		final StampAction stampAction = new StampAction(messages, document);

		final ViewFileAction viewFileAction;
		if (Desktop.isDesktopSupported()
				&& Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) {
			viewFileAction = new ViewFileAction(tailer, messages,
					errorReporter, progress);
		} else {
			viewFileAction = null;
		}

		final EditFileAction editFileAction;
		if (Desktop.isDesktopSupported()
				&& Desktop.getDesktop().isSupported(Desktop.Action.EDIT)) {
			editFileAction = new EditFileAction(tailer, messages,
					errorReporter, progress);
		} else {
			editFileAction = null;
		}

		final ToggleWrapAction toggleWrapAction = new ToggleWrapAction(this,
				messages);

		configurationList = new JComboBox();

		tailMenu = new JPopupMenu(
				messages.getMessage("nu.zoom.catonine.tail.popuplabel"));
		JToolBar toolbar = new JToolBar();

		// Build toolbar

		if (additionalToolbarComponents != null) {
			for (Component component : additionalToolbarComponents) {
				toolbar.add(component);
			}
		}
		toolbar.add(new JButton(stampAction));
		if (viewFileAction != null) {
			toolbar.add(new JButton(viewFileAction));
		}
		if (editFileAction != null) {
			toolbar.add(new JButton(editFileAction));
		}
		toolbar.add(tailControl);
		toolbar.add(new JButton(toggleWrapAction));
		toolbar.add(Box.createHorizontalStrut(16));
		toolbar.add(new JLabel(messages
				.getMessage("nu.zoom.catonine.configurations.select")));
		toolbar.add(configurationList);

		if (this.styleRuleEditCallback != null) {
			toolbar.add(new EditStyleRuleAction(this, messages));
		}

		toolbar.add(Box.createHorizontalGlue());
		tailerMessageLabel = new JLabel(tailerWaitingIcon);
		toolbar.add(tailerMessageLabel);

		final ToggleToolbarAction toggleToolbarAction = new ToggleToolbarAction(
				toolbar, messages);

		final ToggleGutterAction toggleGutterAction = new ToggleGutterAction(
				this, messages);

		createSimpleRuleAction = new CreateSimpleRuleAction(messages, this);

		// Build popup menu
		final String copyactionName = DefaultEditorKit.copyAction;
		if ((copyactionName != null) && (copyactionName.length() > 0)) {
			final EditorKit editorKit = textPane.getEditorKit();
			final Action[] editorKitActions = editorKit.getActions();
			for (final Action editorKitAction : editorKitActions) {
				if (copyactionName
						.equals(editorKitAction.getValue(Action.NAME))) {
					copyToClipboardAction = new CopyToClipboardAction(
							editorKitAction, messages);
					tailMenu.add(copyToClipboardAction);
				}
			}
		}
		tailMenu.add(createSimpleRuleAction);
		tailMenu.add(new JSeparator());

		tailMenu.add(tailControlAction);
		tailMenu.add(stampAction);
		tailMenu.add(toggleWrapAction);
		tailMenu.add(new JSeparator());
		if (viewFileAction != null) {
			tailMenu.add(viewFileAction);
		}
		if (editFileAction != null) {
			tailMenu.add(editFileAction);
		}
		tailMenu.add(new JCheckBoxMenuItem(toggleGutterAction));
		tailMenu.add(toggleToolbarAction);

		add(toolbar, BorderLayout.NORTH);

		tailer.addListener(this);

		// Register keybindings
		registerKeyboardAction(tailControlAction, KeyStroke.getKeyStroke(
				KeyEvent.VK_F, InputEvent.SHIFT_DOWN_MASK),
				WHEN_IN_FOCUSED_WINDOW);
		registerKeyboardAction(stampAction, KeyStroke.getKeyStroke(
				KeyEvent.VK_T, InputEvent.CTRL_DOWN_MASK),
				WHEN_IN_FOCUSED_WINDOW);
		registerKeyboardAction(toggleToolbarAction, KeyStroke.getKeyStroke(
				KeyEvent.VK_L, InputEvent.CTRL_DOWN_MASK),
				WHEN_IN_FOCUSED_WINDOW);
		registerKeyboardAction(toggleGutterAction, KeyStroke.getKeyStroke(
				KeyEvent.VK_G, InputEvent.CTRL_DOWN_MASK),
				WHEN_IN_FOCUSED_WINDOW);
		registerKeyboardAction(toggleWrapAction, KeyStroke.getKeyStroke(
				KeyEvent.VK_A, InputEvent.CTRL_DOWN_MASK),
				WHEN_IN_FOCUSED_WINDOW);

	}

	public void setStyleRulesManager(StyleRulesManager newManager) {
		this.manager = newManager;
		this.listModel = new StyleRuleModel(manager);
		configurationList.setModel(this.listModel);
		this.document.setConfiguration(null);
	}

	public synchronized void setPreferredConfiguration(final String uuid) {
		if (uuid == null) {
			throw new NullPointerException("UUID can not be null");
		}
		List<StyleRules> styleRulesList = manager.getStyleRules();
		for (StyleRules styleRules : styleRulesList) {
			if (uuid.equals(styleRules.getGuid())) {
				// Set selected style rules
				listModel.setSelectedItem(styleRules);
			}
		}
	}

	public void start() {
		tailer.start();
	}

	public void stop() throws IOException {
		this.linesRunner.stop();
		if (tailer != null) {
			tailer.stop();
		}
	}

	public synchronized void setFile(final File fileToTail)
			throws IllegalArgumentException, FileNotFoundException, IOException {
		tailer.setFile(fileToTail);
	}

	public void setFollow(final boolean shouldFollow) {
		follow.set(shouldFollow);
		if (shouldFollow) {
			textPane.setCaretPosition(document.getLength());
			textPane.scrollRectToVisible(new Rectangle(0,
					textPane.getHeight() - 2, 1, 1));
			tailControlAction.setActive();
		} else {
			tailControlAction.setInactive();
		}
	}

	public boolean isFollow() {
		return follow.get();
	}

	public boolean isWrap() {
		return wrap.get();
	}

	public void toggleWrap(final ToggleWrapAction callback) {
		EventQueue.invokeLater(new Runnable() {

			@Override
			public void run() {
				final boolean newWrapState = !isWrap();
				final File file = tailer.getFile();
				if (file != null) {
					try {
						preferences.setWrapForFile(file, newWrapState);
					} catch (InvalidDataTypeException e) {
					} catch (BackendException e) {
					}
				}
				if (newWrapState) {
					noWrapPanel.remove(textPane);
					textScroller.getViewport().remove(noWrapPanel);
					textScroller.getViewport().add(textPane);
					validate();
				} else {
					textScroller.getViewport().remove(textPane);
					textScroller.getViewport().add(noWrapPanel);
					noWrapPanel.add(textPane, BorderLayout.CENTER);
					validate();
				}
				wrap.set(newWrapState);
				callback.stateChanged(newWrapState);
			}
		});
	}

	public boolean isGutterVisible() {
		return gutterVisible.get();
	}

	private void showGutter() {
		if (this.textScroller.getRowHeader() == null) {
			this.gutterview = new GutterView(this.textPane, this.document,
					this.foldedImage, this.unfoldedImage, this.foldImageWidth);
			this.textScroller.setRowHeaderView(this.gutterview);
		}
	}

	private void hideGutter() {
		this.textScroller.setRowHeader(null);
		this.gutterview = null;
	}

	public void setGutterVisible(final boolean gutterVisible) {
		this.gutterVisible.set(gutterVisible);
		final File file = tailer.getFile();
		if (file != null) {
			try {
				this.preferences.setGutterForFile(file, gutterVisible);
			} catch (InvalidDataTypeException e) {
			} catch (BackendException e) {
			}
		}
		if (gutterVisible) {
			EventQueue.invokeLater(new Runnable() {

				@Override
				public void run() {
					showGutter();
				}
			});
		} else {
			EventQueue.invokeLater(new Runnable() {

				@Override
				public void run() {
					hideGutter();
				}
			});
		}
	}

	void editCurrentStyleRules() {
		final Object selectedItem = (this.listModel != null) ? this.listModel
				.getSelectedItem() : null;
		if ((selectedItem != null) && (selectedItem instanceof StyleRules)) {
			this.styleRuleEditCallback
					.editStyleRules((StyleRules) selectedItem);
		}
	}

	class StyleRuleModel extends StyleRulesListModel {

		private static final long serialVersionUID = 1L;

		public StyleRuleModel(StyleRulesManager manager) {
			super(manager, true);
		}

		@Override
		public void setSelectedItem(Object anObject) {
			super.setSelectedItem(anObject);
			if (selectedConfiguration != null) {
				selectedConfiguration.removeStyleRuleListener(TailPane.this);
				selectedConfiguration.removeListener(TailPane.this);
			}
			if (anObject instanceof StyleRules) {
				selectedConfiguration = (StyleRules) anObject;
				selectedConfiguration.addStyleRuleListener(TailPane.this);
				selectedConfiguration.addListener(TailPane.this);
				try {
					preferences.setPreferredUUIDForFile(tailer.getFile(),
							selectedConfiguration.getGuid());
				} catch (InvalidDataTypeException ex) {
					// Ignore
				} catch (BackendException ex) {
					// Ignore
				}
			} else {
				selectedConfiguration = null;
				try {
					preferences.setPreferredUUIDForFile(tailer.getFile(), null);
				} catch (InvalidDataTypeException ex) {
					// Ignore
				} catch (BackendException ex) {
					// Ignore
				}
			}
			document.setConfiguration(selectedConfiguration);
			updateTailerWithListData();
			restyle();
		}
	}

	@Override
	public void listStructureChanged(StyleRules list) {
		list.removeStyleRuleListener(this);
		list.addStyleRuleListener(this);
	}

	@Override
	public void listChangedName(final StyleRules list) {
		// This is handled by the configurationLists style rule model
	}

	@Override
	public void patternChanged(StyleRule pattern) {
		restyle();
	}

	private void updateTailerWithListData() {
		if (selectedConfiguration != null) {
			tailer.setBlockMatcherStarting(selectedConfiguration
					.isLogBlockPatternBeginsBlock());
			tailer.setCharSet(selectedConfiguration.getFileEncoding());
			tailer.setLogBlockRegularExpression(selectedConfiguration
					.getLogBlockPattern());
		} else {
			tailer.setDefaultPattern();
		}
	}

	@Override
	public void listDataChanged(StyleRules list) {
		updateTailerWithListData();
	}

	private void restyle() {
		document.setupStyles();
		if (gutterview != null) {
			gutterview.restyle();
		}
		restartTailer();
	}

	private void restartTailer() {
		if (!this.restartScheduled.get()) {
			restartScheduled.set(true);
			restartTimer.schedule(new RestartTailerTask(), RESTART_WAIT_TIME);
		}
	}

	@Override
	public void lines(final TailerLinesEvent event) {
		linesRunner.run(event.entries, isFollow());
	}

	private void busy() {
		tailerMessageLabel.setIcon(tailerBusyIcon);
		cursorSwitcher.scheduleWaitCursor(this);
		tailerMessageLabel.repaint();
	}

	@Override
	public void tailerWorking() {
		EventQueue.invokeLater(new Runnable() {

			@Override
			public void run() {
				busy();
			}
		});
	}

	private void stopped() {
		tailerMessageLabel.setIcon(tailerStoppedIcon);
		cursorSwitcher.cancelScheduleAndSetDefaultCursor(this);
		tailerMessageLabel.repaint();
	}

	@Override
	public void tailerStopped() {
		EventQueue.invokeLater(new Runnable() {

			@Override
			public void run() {
				stopped();
			}
		});
	}

	private void waiting() {
		tailerMessageLabel.setIcon(tailerWaitingIcon);
		cursorSwitcher.cancelScheduleAndSetDefaultCursor(this);
		tailerMessageLabel.repaint();
	}

	@Override
	public void tailerIdle() {
		EventQueue.invokeLater(new Runnable() {

			@Override
			public void run() {
				waiting();
			}
		});
	}

	class TailerStopperListener implements MouseListener, MouseWheelListener {

		@Override
		public void mouseClicked(MouseEvent e) {
		}

		@Override
		public void mouseEntered(MouseEvent e) {
		}

		@Override
		public void mouseExited(MouseEvent e) {
		}

		@Override
		public void mousePressed(MouseEvent e) {
			// Mouse pressed is popup trigger on linux.
			if (e.isPopupTrigger()) {
				Component source = (Component) e.getSource();
				showTailerPopupMenu(e, source);
			} else {
				if (e.getButton() != MouseEvent.BUTTON3) {
					stopFollowing();
				}
			}
		}

		@Override
		public void mouseReleased(MouseEvent e) {
			// Mouse released is popup trigger on windows.
			if (e.isPopupTrigger()) {
				Component source = (Component) e.getSource();
				showTailerPopupMenu(e, source);
			} else {
				if (e.getButton() != MouseEvent.BUTTON3) {
					stopFollowing();
				}
			}
		}

		private void showTailerPopupMenu(MouseEvent e, Component source) {
			final String selectedText = textPane.getSelectedText();
			if (copyToClipboardAction != null) {
				copyToClipboardAction.setEnabled(selectedText != null);
			}
			createSimpleRuleAction.setEnabled(selectedText != null);
			tailMenu.show(source, e.getX(), e.getY());
		}

		@Override
		public void mouseWheelMoved(MouseWheelEvent e) {
			stopFollowing();
		}

		private void stopFollowing() {
			setFollow(false);
		}
	}

	@Override
	public void tailerHasBeenReset() {
		document.tailerHasBeenReset();
	}

	public void createSimpleRule() {
		final Object selectedItem = (this.listModel != null) ? this.listModel
				.getSelectedItem() : null;
		if ((selectedItem != null) && (selectedItem instanceof StyleRules)) {
			final String selectedText = textPane.getSelectedText();
			if ((selectedText != null) && (selectedText.length() > 0)) {
				this.styleRuleEditCallback.createSimpleRule(
						(StyleRules) selectedItem, selectedText);
			}
		}
	}

	class RestartTailerTask extends TimerTask {

		@Override
		public void run() {
			tailer.restart();
			restartScheduled.set(false) ;
		}

	}
}
